(ns clj-http.test.core-test
  (:require [cheshire.core :as json]
            [clj-http.client :as client]
            [clj-http.conn-mgr :as conn]
            [clj-http.core :as core]
            [clj-http.util :as util]
            [clojure.java.io :refer [file]]
            [clojure.pprint :as pp]
            [clojure.test :refer :all]
            [ring.adapter.jetty :as ring])
  (:import (java.io ByteArrayInputStream)
           (java.net SocketTimeoutException)
           (java.util.concurrent TimeoutException TimeUnit)
           (org.apache.hc.core5.http.message BasicHeader BasicHeaderIterator)
           (org.apache.hc.client5.http.classic.methods HttpPost)
           (org.apache.hc.client5.http.cookie MalformedCookieException)
           (org.apache.hc.core5.http.protocol HttpCoreContext)
           (org.apache.hc.client5.http.protocol HttpClientContext)
           (org.apache.hc.client5.http.impl.classic CloseableHttpClient)
           (org.apache.hc.client5.http.impl.cookie CookieSpecBase RFC6265CookieSpecProvider)
           (org.apache.hc.client5.http.config RequestConfig)
           (org.apache.hc.core5.http HttpRequest HttpResponse HttpConnection
                                     ProtocolException)
           (org.apache.hc.core5.http.protocol HttpContext)
           (org.apache.logging.log4j LogManager)
           (sun.security.provider.certpath SunCertPathBuilderException)))

(defonce logger (LogManager/getLogger "clj-http.test.core-test"))

(defn handler [req]
  (condp = [(:request-method req) (:uri req)]
    [:get "/get"]
    {:status 200 :body "get"}
    [:get "/dont-cache"]
    {:status 200 :body "nocache"
     :headers {"cache-control" "private"}}
    [:get "/empty"]
    {:status 200 :body nil}
    [:get "/empty-gzip"]
    {:status 200 :body nil
     :headers {"content-encoding" "gzip"}}
    [:get "/clojure"]
    {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}"
     :headers {"content-type" "application/clojure"}}
    [:get "/edn"]
    {:status 200 :body "{:foo \"bar\" :baz 7M :eggplant {:quux #{1 2 3}}}"
     :headers {"content-type" "application/edn"}}
    [:get "/clojure-bad"]
    {:status 200 :body "{:foo \"bar\" :baz #=(+ 1 1)}"
     :headers {"content-type" "application/clojure"}}
    [:get "/json"]
    {:status 200 :body "{\"foo\":\"bar\"}"
     :headers {"content-type" "application/json"}}
    [:get "/json-array"]
    {:status 200 :body "[\"foo\", \"bar\"]"
     :headers {"content-type" "application/json"}}
    [:get "/json-bad"]
    {:status 400 :body "{\"foo\":\"bar\"}"}
    [:get "/redirect"]
    {:status 302
     :headers {"location" "http://localhost:18080/redirect"}}
    [:get "/bad-redirect"]
    {:status 301 :headers {"location" "https:///"}}
    [:get "/redirect-to-get"]
    {:status 302
     :headers {"location" "http://localhost:18080/get"}}
    [:get "/unmodified-resource"]
    {:status 304}
    [:get "/transit-json"]
    {:status 200 :body (str "[\"^ \",\"~:eggplant\",[\"^ \",\"~:quux\","
                            "[\"~#set\",[1,3,2]]],\"~:baz\",\"~f7\","
                            "\"~:foo\",\"bar\"]")
     :headers {"content-type" "application/transit+json"}}
    [:get "/transit-json-bad"]
    {:status 400 :body "[\"^ \", \"~:foo\",\"bar\"]"}
    [:get "/transit-json-empty"]
    {:status 200
     :headers {"content-type" "application/transit+json"}}
    [:get "/transit-msgpack"]
    {:status 200
     :body (->> [-125 -86 126 58 101 103 103 112 108 97 110 116 -127 -90 126
                 58 113 117 117 120 -110 -91 126 35 115 101 116 -109 1 3 2
                 -91 126 58 98 97 122 -93 126 102 55 -91 126 58 102 111 111
                 -93 98 97 114]
                (map byte)
                (byte-array)
                (ByteArrayInputStream.))
     :headers {"content-type" "application/transit+msgpack"}}
    [:head "/head"]
    {:status 200}
    [:get "/content-type"]
    {:status 200 :body (:content-type req)}
    [:get "/header"]
    {:status 200 :body (get-in req [:headers "x-my-header"])}
    [:post "/post"]
    {:status 200 :body (:body req)}
    [:get "/error"]
    {:status 500 :body "o noes"}
    [:get "/timeout"]
    (do
      (Thread/sleep 10)
      {:status 200 :body "timeout"})
    [:delete "/delete-with-body"]
    {:status 200 :body "delete-with-body"}
    [:post "/multipart"]
    {:status 200 :body (:body req)}
    [:get "/get-with-body"]
    {:status 200 :body (:body req)}
    [:options "/options"]
    {:status 200 :body "options"}
    [:copy "/copy"]
    {:status 200 :body "copy"}
    [:move "/move"]
    {:status 200 :body "move"}
    [:patch "/patch"]
    {:status 200 :body "patch"}
    [:get "/headers"]
    {:status 200 :body (json/encode (:headers req))}
    [:propfind "/propfind"]
    {:status 200 :body "propfind"}
    [:propfind "/propfind-with-body"]
    {:status 200 :body (:body req)}
    [:get "/query-string"]
    {:status 200 :body (:query-string req)}
    [:get "/cookie"]
    {:status 200 :body "yay" :headers {"Set-Cookie" "foo=bar"}}
    [:get "/bad-cookie"]
    {:status 200 :body "yay"
     :headers
     {"Set-Cookie"
      (str "DD-PSHARD=3; expires=\"Thu, 12-Apr-2018 06:40:25 GMT\"; "
           "Max-Age=604800; Path=/; secure; HttpOnly")}}))

(defn add-headers-if-requested [client]
  (fn [req]
    (let [resp (client req)
          add-all (-> req :headers (get "add-headers"))
          headers (:headers resp)]
      (if add-all
        (assoc resp :headers (assoc headers "got" (pr-str (:headers req))))
        resp))))

(defn run-server
  []
  (defonce server
    (ring/run-jetty (add-headers-if-requested handler) {:port 18080 :join? false})))

(defn localhost [path]
  (str "http://localhost:18080" path))

(def base-req
  {:scheme :http
   :server-name "localhost"
   :server-port 18080})

(defn request [req]
  (core/request (merge base-req req)))

(defn slurp-body [req]
  (slurp (:body req)))

(deftest ^:integration makes-get-request
  (run-server)
  (let [resp (request {:request-method :get :uri "/get"})]
    (is (= 200 (:status resp)))
    (is (= "get" (slurp-body resp)))))

(deftest ^:integration save-request-option
  (run-server)
  (let [resp (request {:request-method :post
                       :uri "/post"
                       :body (util/utf8-bytes "contents")
                       :save-request? true})]
    (is (= "/post" (-> resp :request :uri)))))

(deftest ^:integration makes-head-request
  (run-server)
  (let [resp (request {:request-method :head :uri "/head"})]
    (is (= 200 (:status resp)))
    (is (nil? (:body resp)))))

(deftest ^:integration sets-content-type-with-charset
  (run-server)
  (let [resp (client/request {:scheme :http
                              :server-name "localhost"
                              :server-port 18080
                              :request-method :get :uri "/content-type"
                              :content-type "text/plain"
                              :character-encoding "UTF-8"})]
    (is (= "text/plain; charset=UTF-8" (:body resp)))))

(deftest ^:integration sets-content-type-without-charset
  (run-server)
  (let [resp (client/request {:scheme :http
                              :server-name "localhost"
                              :server-port 18080
                              :request-method :get :uri "/content-type"
                              :content-type "text/plain"})]
    (is (= "text/plain" (:body resp)))))

(deftest ^:integration sets-arbitrary-headers
  (run-server)
  (let [resp (request {:request-method :get :uri "/header"
                       :headers {"x-my-header" "header-val"}})]
    (is (= "header-val" (slurp-body resp)))))

(deftest ^:integration sends-and-returns-byte-array-body
  (run-server)
  (let [resp (request {:request-method :post :uri "/post"
                       :body (util/utf8-bytes "contents")})]
    (is (= 200 (:status resp)))
    (is (= "contents" (slurp-body resp)))))

(deftest ^:integration returns-arbitrary-headers
  (run-server)
  (let [resp (request {:request-method :get :uri "/get"})]
    (is (string? (get-in resp [:headers "date"])))
    (is (nil? (get-in resp [:headers "Date"])))))

(deftest ^:integration returns-status-on-exceptional-responses
  (run-server)
  (let [resp (request {:request-method :get :uri "/error"})]
    (is (= 500 (:status resp)))))

(deftest ^:integration sets-socket-timeout
  (run-server)
  (try
    (is (thrown? SocketTimeoutException
                 (client/request {:scheme :http
                                  :server-name "localhost"
                                  :server-port 18080
                                  :request-method :get :uri "/timeout"
                                  :socket-timeout 1})))))

(deftest ^:integration delete-with-body
  (run-server)
  (let [resp (request {:request-method :delete :uri "/delete-with-body"
                       :body (.getBytes "foo bar")})]
    (is (= 200 (:status resp)))))

(deftest ^:integration self-signed-ssl-get
  (let [server (ring/run-jetty handler
                               {:port 8081 :ssl-port 18082
                                :ssl? true
                                :join? false
                                :keystore "test-resources/keystore"
                                :key-password "keykey"})]
    (try
      (is (thrown? SunCertPathBuilderException
                   (client/request {:scheme :https
                                    :server-name "localhost"
                                    :server-port 18082
                                    :request-method :get :uri "/get"})))
      (let [resp (request {:request-method :get :uri "/get" :server-port 18082
                           :scheme :https :insecure? true})]
        (is (= 200 (:status resp)))
        (is (= "get" (String. (util/force-byte-array (:body resp))))))
      (finally
        (.stop server)))))

(deftest ^:integration multipart-form-uploads
  (run-server)
  (let [bytes (util/utf8-bytes "byte-test")
        stream (ByteArrayInputStream. bytes)
        resp (request {:request-method :post :uri "/multipart"
                       :multipart [{:name "a" :content "testFINDMEtest"
                                    :encoding "UTF-8"
                                    :mime-type "application/text"}
                                   {:name "b" :content bytes
                                    :mime-type "application/json"}
                                   {:name "d"
                                    :content (file "test-resources/keystore")
                                    :encoding "UTF-8"
                                    :mime-type "application/binary"}
                                   {:name "c" :content stream
                                    :mime-type "application/json"}
                                   {:name "e" :part-name "eggplant"
                                    :content "content"
                                    :mime-type "application/text"}]})
        resp-body (apply str (map #(try (char %) (catch Exception _ ""))
                                  (util/force-byte-array (:body resp))))]
    (is (= 200 (:status resp)))
    (is (re-find #"testFINDMEtest" resp-body))
    (is (re-find #"application/json" resp-body))
    (is (re-find #"application/text" resp-body))
    (is (re-find #"UTF-8" resp-body))
    (is (re-find #"byte-test" resp-body))
    (is (re-find #"name=\"c\"" resp-body))
    (is (re-find #"name=\"d\"" resp-body))
    (is (re-find #"name=\"eggplant\"" resp-body))
    (is (re-find #"content" resp-body))))

(deftest ^:integration multipart-inputstream-length
  (run-server)
  (let [bytes (util/utf8-bytes "byte-test")
        stream (ByteArrayInputStream. bytes)
        resp (request {:request-method :post :uri "/multipart"
                       :multipart [{:name "c" :content stream :length 9
                                    :mime-type "application/json"}]})
        resp-body (apply str (map #(try (char %) (catch Exception _ ""))
                                  (util/force-byte-array (:body resp))))]
    (is (= 200 (:status resp)))
    (is (re-find #"byte-test" resp-body))))

(deftest parse-headers
  (are [headers expected]
      (let [iterator (BasicHeaderIterator.
                      (into-array BasicHeader
                                  (map (fn [[name value]]
                                         (BasicHeader. name value))
                                       headers)) nil)]
        (is (= (core/parse-headers iterator) expected)))

    [] {}

    [["Set-Cookie" "one"]] {"set-cookie" "one"}

    [["Set-Cookie" "one"] ["set-COOKIE" "two"]]
    {"set-cookie" ["one" "two"]}

    [["Set-Cookie" "one"] ["serVer" "some-server"] ["set-cookie" "two"]]
    {"set-cookie" ["one" "two"] "server" "some-server"}))

(deftest ^:integration t-streaming-response
  (run-server)
  (let [stream (:body (request {:request-method :get :uri "/get" :as :stream}))
        body (slurp stream)]
    (is (= "get" body))))


(deftest ^:integration throw-on-too-many-redirects
  (run-server)
  (let [resp (client/get (localhost "/redirect")
                         {:max-redirects 2 :throw-exceptions false
                          :redirect-strategy :none
                          :allow-circular-redirects true})]
    (is (= 302 (:status resp))))

  (let [resp (client/get (localhost "/redirect")
                         {:max-redirects 3
                          :redirect-strategy :graceful
                          :allow-circular-redirects true})]
    (is (= 302 (:status resp)))
    (is (= 3 (count (:trace-redirects resp))))
    (is (=  ["http://localhost:18080/redirect"
             "http://localhost:18080/redirect"
             "http://localhost:18080/redirect"]
            (:trace-redirects resp))))

  (is (thrown-with-msg? Exception #"Maximum redirects \(2\) exceeded"
                        (client/get (localhost "/redirect")
                                    {:max-redirects 2
                                     :throw-exceptions true
                                     :allow-circular-redirects true})))
  (is (thrown-with-msg? Exception #"Maximum redirects \(50\) exceeded"
                        (client/get (localhost "/redirect")
                                    {:throw-exceptions true
                                     :allow-circular-redirects true}))))

(deftest ^:integration get-with-body
  (run-server)
  (let [resp (request {:request-method :get :uri "/get-with-body"
                       :body (.getBytes "foo bar")})]
    (is (= 200 (:status resp)))
    (is (= "foo bar" (String. (util/force-byte-array (:body resp)))))))

(deftest ^:integration head-with-body
  (run-server)
  (let [resp (request {:request-method :head :uri "/head" :body "foo"})]
    (is (= 200 (:status resp)))))

(deftest ^:integration t-clojure-output-coercion
  (run-server)
  (let [resp (client/get (localhost "/clojure") {:as :clojure})]
    (is (= 200 (:status resp)))
    (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}} (:body resp))))
  (let [clj-resp (client/get (localhost "/clojure") {:as :auto})
        edn-resp (client/get (localhost "/edn") {:as :auto})]
    (is (= 200 (:status clj-resp) (:status edn-resp)))
    (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}}
           (:body clj-resp)
           (:body edn-resp)))))

(deftest ^:integration t-transit-output-coercion
  (run-server)
  (let [transit-json-resp (client/get (localhost "/transit-json") {:as :auto})
        transit-msgpack-resp (client/get (localhost "/transit-msgpack")
                                         {:as :auto})
        bad-status-resp-default
        (client/get (localhost "/transit-json-bad")
                    {:throw-exceptions false :as :transit+json})
        bad-status-resp-always
        (client/get (localhost "/transit-json-bad")
                    {:throw-exceptions false :as :transit+json
                     :coerce :always})
        bad-status-resp-exceptional
        (client/get (localhost "/transit-json-bad")
                    {:throw-exceptions false :as :transit+json
                     :coerce :exceptional})
        empty-resp (client/get (localhost "/transit-json-empty")
                               {:throw-exceptions false :as :transit+json})]
    (is (= 200
           (:status transit-json-resp)
           (:status transit-msgpack-resp)
           (:status empty-resp)))
    (is (= 400
           (:status bad-status-resp-default)
           (:status bad-status-resp-always)
           (:status bad-status-resp-exceptional)))
    (is (= {:foo "bar" :baz 7M :eggplant {:quux #{1 2 3}}}
           (:body transit-json-resp)
           (:body transit-msgpack-resp)))

    (is (nil? (:body empty-resp)))

    (is (= "[\"^ \", \"~:foo\",\"bar\"]"
           (:body bad-status-resp-default)))
    (is (= {:foo "bar"}
           (:body bad-status-resp-always)))
    (is (= {:foo "bar"}
           (:body bad-status-resp-exceptional)))))

(deftest ^:integration t-json-output-coercion
  (run-server)
  (let [resp (client/get (localhost "/json") {:as :json})
        resp-array (client/get (localhost "/json-array") {:as :json-strict})
        resp-str (client/get (localhost "/json")
                             {:as :json :coerce :exceptional})
        resp-str-keys (client/get (localhost "/json") {:as :json-string-keys})
        resp-strict-str-keys (client/get (localhost "/json")
                                         {:as :json-strict-string-keys})
        resp-auto (client/get (localhost "/json") {:as :auto})
        bad-resp (client/get (localhost "/json-bad")
                             {:throw-exceptions false :as :json})
        bad-resp-json (client/get (localhost "/json-bad")
                                  {:throw-exceptions false :as :json
                                   :coerce :always})
        bad-resp-json2 (client/get (localhost "/json-bad")
                                   {:throw-exceptions false :as :json
                                    :coerce :unexceptional})]
    (is (= 200
           (:status resp)
           (:status resp-array)
           (:status resp-str)
           (:status resp-str-keys)
           (:status resp-strict-str-keys)
           (:status resp-auto)))
    (is (= {:foo "bar"}
           (:body resp)
           (:body resp-auto)))
    (is (= ["foo", "bar"]
           (:body resp-array)))
    (is (= {"foo" "bar"}
           (:body resp-strict-str-keys)
           (:body resp-str-keys)))
    ;; '("foo" "bar") and ["foo" "bar"] compare as equal with =.
    (is (vector? (:body resp-array)))
    (is (= "{\"foo\":\"bar\"}" (:body resp-str)))
    (is (= 400
           (:status bad-resp)
           (:status bad-resp-json)
           (:status bad-resp-json2)))
    (is (= "{\"foo\":\"bar\"}" (:body bad-resp))
        "don't coerce on bad response status by default")
    (is (= {:foo "bar"} (:body bad-resp-json)))
    (is (= "{\"foo\":\"bar\"}" (:body bad-resp-json2)))))

(deftest ^:integration t-ipv6
  (run-server)
  (let [resp (client/get "http://[::1]:18080/get")]
    (is (= 200 (:status resp)))
    (is (= "get" (:body resp)))))

(deftest t-custom-retry-handler
  (let [called? (atom false)]
    (is (thrown? Exception
                 (client/post "http://localhost"
                              {:multipart [{:name "title" :content "Foo"}
                                           {:name "Content/type"
                                            :content "text/plain"}
                                           {:name "file"
                                            :content (file "/tmp/missingfile")}]
                               :retry-handler (fn [ex try-count http-context]
                                                (reset! called? true)
                                                false)})))
    (is @called?)))

;; super-basic test for methods that aren't used that often
(deftest ^:integration t-copy-options-move
  (run-server)
  (let [resp1 (client/options (localhost "/options"))
        resp2 (client/move (localhost "/move"))
        resp3 (client/copy (localhost "/copy"))
        resp4 (client/patch (localhost "/patch"))]
    (is (= #{200} (set (map :status [resp1 resp2 resp3 resp4]))))
    (is (= "options" (:body resp1)))
    (is (= "move" (:body resp2)))
    (is (= "copy" (:body resp3)))
    (is (= "patch" (:body resp4)))))

(deftest ^:integration t-json-encoded-form-params
  (run-server)
  (let [params {:param1 "value1" :param2 {:foo "bar"}}
        resp (client/post (localhost "/post") {:content-type :json
                                               :form-params params})]
    (is (= 200 (:status resp)))
    (is (= (json/encode params) (:body resp)))))

(deftest ^:integration t-request-interceptor
  (run-server)
  (let [req-ctx (atom [])
        {:keys [status trace-redirects] :as resp}
        (client/get
         (localhost "/get")
         {:request-interceptor
          (fn [^HttpRequest req ^HttpContext ctx]
            (reset! req-ctx {:method (.getMethod req) :uri (.getURI req)}))})]
    (is (= 200 status))
    (is (= "GET" (:method @req-ctx)))
    (is (= "/get" (.getPath (:uri @req-ctx))))))

(deftest ^:integration t-response-interceptor
  (run-server)
  (let [saved-ctx (atom [])
        {:keys [status trace-redirects] :as resp}
        (client/get
         (localhost "/redirect-to-get")
         {:response-interceptor
          (fn [^HttpResponse resp ^HttpContext ctx]
            (let [conn
                  (.getAttribute ctx "http.connection")]
              (swap! saved-ctx conj {:remote-port (.getRemotePort conn)
                                     :http-conn conn})))})]
    (is (= 200 status))
    (is (= 2 (count @saved-ctx)))
    #_(is (= (count trace-redirects) (count @saved-ctx)))
    (is (every? #(= 18080 (:remote-port %)) @saved-ctx))
    (is (every? #(instance? HttpConnection (:http-conn %)) @saved-ctx))))

(deftest ^:integration t-send-input-stream-body
  (run-server)
  (let [b1 (:body (client/post "http://localhost:18080/post"
                               {:body (ByteArrayInputStream. (.getBytes "foo"))
                                :length 3}))
        b2 (:body (client/post "http://localhost:18080/post"
                               {:body (ByteArrayInputStream.
                                       (.getBytes "foo"))}))
        b3 (:body (client/post "http://localhost:18080/post"
                               {:body (ByteArrayInputStream.
                                       (.getBytes "apple"))
                                :length 2}))]
    (is (= b1 "foo"))
    (is (= b2 "foo"))
    (is (= b3 "ap"))))

;; (deftest t-add-client-params
;;   (testing "Using add-client-params!"
;;     (let [ps {"http.conn-manager.timeout" 100
;;               "http.socket.timeout" 250
;;               "http.protocol.allow-circular-redirects" false
;;               "http.protocol.version" HttpVersion/HTTP_1_0
;;               "http.useragent" "clj-http"}
;;           setps (.getParams (doto (DefaultHttpClient.)
;;                               (core/add-client-params! ps)))]
;;       (doseq [[k v] ps]
;;         (is (= v (.getParameter setps k)))))))

;; Regression, get notified if something changes
#_
(deftest ^:integration t-known-client-params-are-unchanged
  (let [params ["http.socket.timeout" CoreConnectionPNames/SO_TIMEOUT
                "http.connection.timeout"
                CoreConnectionPNames/CONNECTION_TIMEOUT
                "http.protocol.version" CoreProtocolPNames/PROTOCOL_VERSION
                "http.useragent" CoreProtocolPNames/USER_AGENT
                "http.conn-manager.timeout" ClientPNames/CONN_MANAGER_TIMEOUT
                "http.protocol.allow-circular-redirects"
                ClientPNames/ALLOW_CIRCULAR_REDIRECTS]]
    (doseq [[plaintext constant] (partition 2 params)]
      (is (= plaintext constant)))))

;; If you don't explicitly set a :cookie-policy, use
;; CookiePolicy/BROWSER_COMPATIBILITY
;; (deftest t-add-client-params-default-cookie-policy
;;   (testing "Using add-client-params! to get a default cookie policy"
;;     (let [setps (.getParams (doto (DefaultHttpClient.)
;;                               (core/add-client-params! {})))]
;;       (is (= CookiePolicy/BROWSER_COMPATIBILITY
;;              (.getParameter setps ClientPNames/COOKIE_POLICY))))))

;; If you set a :cookie-policy, the name of the policy is registered
;; as (str (type cookie-policy))
;; (deftest t-add-client-params-cookie-policy
;;   (testing "Using add-client-params! to get an explicitly set :cookie-policy"
;;     (let [setps (.getParams (doto (DefaultHttpClient.)
;;                               (core/add-client-params!
;;                                {:cookie-policy (constantly nil)})))]
;;       (is (.startsWith ^String
;;                        (.getParameter setps ClientPNames/COOKIE_POLICY)
;;                        "class ")))))


;; This relies on connections to writequit.org being slower than 10ms, if this
;; fails, you must have very nice internet.
(deftest ^:integration sets-conn-timeout
  (run-server)
  (try
    (is (thrown? SocketTimeoutException
                 (client/request {:scheme :http
                                  :server-name "writequit.org"
                                  :server-port 80
                                  :request-method :get :uri "/"
                                  :conn-timeout 10})))))

(deftest ^:integration connection-pool-timeout
  (run-server)
  (client/with-connection-pool {:threads 1 :default-per-route 1}
    (let [async-request #(future (client/request {:scheme :http
                                                  :server-name "localhost"
                                                  :server-port 18080
                                                  :request-method :get
                                                  :conn-timeout 1
                                                  :conn-request-timeout 1
                                                  :uri "/timeout"}))
          is-pool-timeout-error?
          (fn [req-fut]
            #_(instance? org.apache.http.conn.ConnectionPoolTimeoutException
                         (try @req-fut (catch Exception e (.getCause e))))
            (try @req-fut (catch Exception e (.getCause e))))
          req1 (async-request)
          req2 (async-request)
          timeout-error1 (is-pool-timeout-error? req1)
          timeout-error2 (is-pool-timeout-error? req2)]
      (is (or timeout-error1 timeout-error2)))))

(deftest ^:integration t-header-collections
  (run-server)
  (let [headers (-> (client/get "http://localhost:18080/headers"
                                {:headers {"foo" ["bar" "baz"]
                                           "eggplant" "quux"}})
                    :body
                    json/decode)]
    (is (= {"eggplant" "quux" "foo" "bar,baz"}
           (select-keys headers ["foo" "eggplant"])))))

(deftest ^:integration t-clojure-no-read-eval
  (run-server)
  (is (thrown? Exception (client/get (localhost "/clojure-bad") {:as :clojure}))
      "Should throw an exception when reading clojure eval components"))

(deftest ^:integration t-numeric-headers
  (run-server)
  (client/request {:method :get :url (localhost "/get") :headers {"foo" 2}}))

(deftest ^:integration t-empty-response-coercion
  (run-server)
  (let [resp (client/get (localhost "/empty") {:as :clojure})]
    (is (= (:body resp) nil)))
  (let [resp (client/get (localhost "/empty") {:as :json})]
    (is (= (:body resp) nil)))
  (let [resp (client/get (localhost "/empty-gzip")
                         {:as :clojure})]
    (is (= (:body resp) nil)))
  (let [resp (client/get (localhost "/empty-gzip")
                         {:as :json})]
    (is (= (:body resp) nil))))

(deftest ^:integration t-trace-redirects
  (run-server)
  (let [resp-with-redirects
        (client/request {:method :get
                         :url (localhost "/redirect-to-get")})

        resp-with-graceful-redirects
        (client/request {:method :get
                         :url (localhost "/redirect-to-get")
                         :redirect-strategy :graceful})

        resp-without-redirects
        (client/request {:method :get
                         :url (localhost "/redirect-to-get")
                         :redirect-strategy :none})]

    (is (= (:trace-redirects resp-with-redirects)
           ["http://localhost:18080/get"]))

    (is (= (:trace-redirects resp-with-graceful-redirects)
           ["http://localhost:18080/get"]))

    (is (= (:trace-redirects resp-without-redirects) []))))

(deftest ^:integration t-override-request-config
  (run-server)
  (let [called-args (atom [])
        real-http-client core/build-http-client
        http-context (HttpClientContext/create)
        request-config (.build (RequestConfig/custom))]
    (with-redefs
      [core/build-http-client
       (fn [& args]
         (proxy [CloseableHttpClient] []
           (execute [http-req context]
             (swap! called-args conj [http-req context])
             (.execute (apply real-http-client args) http-req context))))]
      (client/request {:method :get
                       :url "http://localhost:18080/get"
                       :http-client-context http-context
                       :http-request-config request-config})

      (let [context-for-request (last (last @called-args))]
        (is (= http-context context-for-request))
        (is (= request-config (.getRequestConfig context-for-request)))))))

(deftest ^:integration test-custom-http-builder-fns
  (run-server)
  (let [resp (client/get (localhost "/get")
                         {:headers {"add-headers" "true"}
                          :http-builder-fns
                          [(fn [builder req]
                             (.setDefaultHeaders builder (:hdrs req)))]
                          :hdrs [(BasicHeader. "foo" "bar")]})]
    (is (= 200 (:status resp)))
    (is (.contains (get-in resp [:headers "got"]) "\"foo\" \"bar\"")
        "Headers should have included the new default headers"))
  (let [resp (promise)
        error (promise)
        f (client/get (localhost "/get")
                      {:async true
                       :headers {"add-headers" "true"}
                       :async-http-builder-fns
                       [(fn [builder req]
                          (.setDefaultHeaders builder (:hdrs req)))]
                       :hdrs [(BasicHeader. "foo" "bar")]}
                      resp error)]
    (.get f)
    (is (= 200 (:status @resp)))
    (is (.contains (get-in @resp [:headers "got"]) "\"foo\" \"bar\"")
        "Headers should have included the new default headers")
    (is (not (realized? error)))))
#_
(deftest ^:integration test-bad-redirects
  (run-server)
  (try
    (client/get (localhost "/bad-redirect"))
    (is false "should have thrown an exception")
    (catch ProtocolException e
      (is (.contains
           (.getMessage e)
           "Redirect URI does not specify a valid host name: https:///"))))
  ;; async version
  (let [e (atom nil)
        latch (promise)]
    (try
      (.get
       (client/get (localhost "/bad-redirect") {:async true}
                   (fn [resp]
                     (is false
                         (str "should not have been called but got" resp)))
                   (fn [err]
                     (reset! e err)
                     (deliver latch true)
                     nil)))
      (catch Exception error
        (is (.contains
             (.getMessage error)
             "Redirect URI does not specify a valid host name: https:///"))))
    @latch
    (is (.contains
         (.getMessage @e)
         "Redirect URI does not specify a valid host name: https:///")))
  (try
    (.get (client/get
           (localhost "/bad-redirect")
           {:async true
            :validate-redirects false}
           (fn [resp]
             (is false
                 (str "should not have been called but got" resp)))
           (fn [err]
             (is false
                 (str "should not have been called but got" err))))
          1 TimeUnit/SECONDS)
    (is false "should have thrown a timeout exception")
    (catch TimeoutException te)))

(deftest ^:integration test-reusable-http-client
  (run-server)
  (let [cm (conn/make-reuseable-async-conn-manager {})
        hc (core/build-async-http-client {} cm)]
    (client/get (localhost "/json")
                {:connection-manager cm
                 :http-client hc
                 :as :json
                 :async true}
                (fn [resp]
                  (is (= 200 (:status resp)))
                  (is (= {:foo "bar"} (:body resp))))
                (fn [e] (is false (str "failed with " e)))))
  (let [cm (conn/make-reusable-conn-manager {})
        hc (:http-client (client/get (localhost "/get")
                                     {:connection-manager cm}))
        resp (client/get (localhost "/json")
                         {:connection-manager cm
                          :http-client hc
                          :as :json})]
    (is (= 200 (:status resp)))
    (is (= {:foo "bar"} (:body resp)))))

(deftest ^:integration t-cookies-spec
  (run-server)
  (try
    (client/get (localhost "/bad-cookie"))
    (is false "should have failed")
    (catch MalformedCookieException e))
  (client/get (localhost "/bad-cookie") {:decode-cookies false})
  (let [validated (atom false)
        spec-provider (RFC6265CookieSpecProvider.)
        resp (client/get (localhost "/cookie")
                         {:cookie-spec
                          (fn [http-context]
                            (proxy [CookieSpecBase] []
                              ;; Version and version header
                              (getVersion [] 0)
                              (getVersionHeader [] nil)
                              ;; parse headers into cookie objects
                              (parse [header cookie-origin]
                                (.parse (.create spec-provider http-context)
                                        header cookie-origin))
                              ;; Validate a cookie, throwing MalformedCookieException if the
                              ;; cookies isn't valid
                              (validate [cookie cookie-origin]
                                (reset! validated true))
                              ;; Determine if a cookie matches the target location
                              (match [cookie cookie-origin] true)
                              ;; Format a list of cookies into a list of headers
                              (formatCookies [cookies] (java.util.ArrayList.))))})]
    (is (= @validated true))))


(deftest t-cache-config
  (let [cc (core/build-cache-config
            {:cache-config {:allow-303-caching true
                            :asynchronous-workers 2
                            :heuristic-caching-enabled true
                            :heuristic-coefficient 1.5
                            :heuristic-default-lifetime 12
                            :max-cache-entries 100
                            :max-object-size 123
                            :max-update-retries 3
                            :never-cache-http10-responses-with-query-string false
                            :shared-cache false
                            :weak-etag-on-put-delete-allowed true}})]
    (is (= true (.is303CachingEnabled cc)))
    (is (= 2 (.getAsynchronousWorkers cc)))
    (is (= true (.isHeuristicCachingEnabled cc)))
    (is (= 1.5 (.getHeuristicCoefficient cc)))
    (is (= 12 (.getHeuristicDefaultLifetime cc)))
    (is (= 100 (.getMaxCacheEntries cc)))
    (is (= 123 (.getMaxObjectSize cc)))
    (is (= 3 (.getMaxUpdateRetries cc)))
    (is (= false (.isNeverCacheHTTP10ResponsesWithQuery cc)))
    (is (= false (.isSharedCache cc)))
    (is (= true (.isWeakETagOnPutDeleteAllowed cc)))))

(deftest ^:integration t-client-caching
  (run-server)
  (let [cm (conn/make-reusable-conn-manager {})
        r1 (client/get (localhost "/get")
                       {:connection-manager cm :cache true})
        client (:http-client r1)
        r2 (client/get (localhost "/get")
                       {:connection-manager cm :http-client client :cache true})
        r3 (client/get (localhost "/get")
                       {:connection-manager cm :http-client client :cache true})
        r4 (client/get (localhost "/get")
                       {:connection-manager cm :http-client client :cache true})]
    (is (= :CACHE_MISS (:cached r1)))
    (is (= :VALIDATED (:cached r2)))
    (is (= :VALIDATED (:cached r3)))
    (is (= :VALIDATED (:cached r4))))
  (let [cm (conn/make-reusable-conn-manager {})
        r1 (client/get (localhost "/dont-cache")
                       {:connection-manager cm :cache true})
        client (:http-client r1)
        r2 (client/get (localhost "/dont-cache")
                       {:connection-manager cm :http-client client :cache true})
        r3 (client/get (localhost "/dont-cache")
                       {:connection-manager cm :http-client client :cache true})
        r4 (client/get (localhost "/dont-cache")
                       {:connection-manager cm :http-client client :cache true})]
    (is (= :CACHE_MISS (:cached r1)))
    (is (= :CACHE_MISS (:cached r2)))
    (is (= :CACHE_MISS (:cached r3)))
    (is (= :CACHE_MISS (:cached r4)))))
